// SPDX-FileCopyrightText: 2018-2023 Giovanni Dante Grazioli <deroad@libero.it>
// SPDX-License-Identifier: BSD-3-Clause

import Base from '../core/base.js';
import Variable from '../core/variable.js';
import Extra from '../core/extra.js';

var _memory_name = "_memory";

var _to_argument = function(wasm_var) {
    return wasm_var.toString('arg');
};

var WasmVar = function(name, type, instr) {
    this.name = name;
    this.type = type;
    this.instr = instr;
    this.toString = function(s) {
        if (s == 'arg') {
            return this.type + ' ' + this.name;
        }
        return this.name;
    };
};

var StackVar = function(type, instr) {
    return new WasmVar(Variable.uniqueName('stack'), type, instr);
};

var _is_next_a_set = function(instr, instructions) {
    var pos = instructions.indexOf(instr) + 1;
    if (!instructions[pos]) {
        return false;
    }
    var mnem = instructions[pos].parsed.mnem;
    return mnem == 'set_local' || mnem == 'set_global' ? pos : -1;
};

var _is_next_local = function(instr, instructions) {
    var pos = instructions.indexOf(instr) + 1;
    if (!instructions[pos]) {
        return false;
    }
    var mnem = instructions[pos].parsed.mnem;
    return mnem == 'set_local' ? pos : -1;
};

var _is_previous_call = function(instr, instructions) {
    var pos = instructions.indexOf(instr) - 1;
    if (!instructions[pos]) {
        return false;
    }
    var mnem = instructions[pos].parsed.mnem;
    return mnem == 'call' ? pos : -1;
};

var _set_local = function(instr, context, instructions, type, allow_args) {
    var pos = _is_next_local(instr, instructions);
    var name = allow_args ? 'arg_' : 'local';
    var n = '';
    if (pos < 0) {
        n = instr.parsed.opd[0];
        if (!context.local[n]) {
            name += n;
            context.local[n] = new WasmVar(name, type, instr);
            if (allow_args) {
                context.input[n] = context.local[n];
            }
        }
        return context.local[n];
    }
    n = instructions[pos].parsed.opd[0];
    if (!context.local[n]) {
        name += n;
        context.local[n] = new WasmVar(name, type, instr);
        if (allow_args) {
            context.input[n] = context.local[n];
        }
    }
    return context.local[n];
};

var _set_global = function(instr, context, instructions, type) {
    var n = instr.parsed.opd[0].trim();
    if (!context.global[n]) {
        context.global[n] = new WasmVar('global_' + instr.parsed.opd[0], type, instr);
    }
    return context.global[n];
};

var _bits = function(o) {
    if (typeof o == 'object') {
        o = o.parsed.type;
    }
    switch (o) {
        case 'f32':
            return 32;
        case 'f64':
            return 64;
        case 'i64':
            return 64;
        default:
            return 32;
    }
};

var _type = function(o, unsigned) {
    if (typeof o == 'object') {
        o = o.parsed.type;
    }
    switch (o) {
        case 'f32':
            return 'float';
        case 'f64':
            return 'double';
        case 'i64':
            return unsigned ? 'uint64_t' : 'int64_t';
        default:
            return unsigned ? 'uint32_t' : 'int32_t';
    }
};

var _remove_const = function(instr, instructions, stackval) {
    if (stackval && instructions.indexOf(stackval.instr) == (instructions.indexOf(instr) - 1) && stackval.instr.parsed.mnem == 'const') {
        stackval.instr.valid = false;
        // <type>.const <N>
        return stackval.instr.parsed.opd[0];
    }
    return stackval;
};

var _math = function(instr, context, instructions, type, op) {
    var source_b = context.stack.pop();
    source_b = _remove_const(instr, instructions, source_b);
    var source_a = context.stack.pop();
    source_a.type = type;
    source_b.type = type;
    var pos = _is_next_a_set(instr, instructions);
    var destination = StackVar(type, instr);
    context.stack.push(destination); // push must happen.
    if (pos < 0) {
        return op(destination.toString(), source_a.toString(), source_b.toString());
    }
    var next_set = instructions[pos];
    next_set.valid = false;
    if (next_set.parsed.mnem == 'set_local') {
        destination = _set_local(next_set, context, instructions, 'int32_t', false);
    } else {
        destination = _set_global(next_set, context, instructions, 'int32_t');
    }
    return op(destination.toString(), source_a.toString(), source_b.toString());
};

var _cmp = {
    eq: 'EQ',
    ne: 'NE',
    eqz: 'EQ',
    nez: 'NE',
    gt_s: 'GT',
    gt_u: 'GT',
    ge_s: 'GE',
    ge_u: 'GE',
    lt_s: 'LT',
    lt_u: 'LT',
    le_s: 'LE',
    le_u: 'LE',
    gt: 'GT',
    ge: 'GE',
    lt: 'LT',
    le: 'LE'
};

var _conditional = function(instr, context, instructions) {
    var cond = {};
    cond.b = context.stack.pop().toString();
    cond.a = context.stack.pop().toString();
    if (!cond.a) {
        cond.a = '?';
    }
    if (!cond.b) {
        cond.b = '?';
    }
    cond.cmp = _cmp[instr.parsed.mnem];
    context.condstack.push(cond);
    return Base.nop();
};

var _conditional_zero = function(instr, context, instructions) {
    var cond = {};
    cond.b = '0';
    cond.a = context.stack.pop().toString();
    if (!cond.a) {
        cond.a = '?';
    }
    cond.cmp = _cmp[instr.parsed.mnem];
    context.condstack.push(cond);
    return Base.nop();
};

var _set_instruction_conditional = function(instr, context) {
    var cond = context.condstack.pop();
    if (!cond) {
        var arg = context.stack.pop();
        if (!arg) {
            arg = '?';
        }
        instr.conditional(arg.toString(), '0', 'NE');
    } else {
        instr.conditional(cond.a, cond.b, cond.cmp);
    }
};

var _common_load = function(instr, context) {
    context.memory = true;
    var offset = instr.parsed.opd[instr.parsed.opd.length - 1];
    var pointer = context.stack.pop().toString();
    var register = StackVar(_type(instr), instr);
    context.stack.push(register);
    if (offset != '0') {
        var bits = instr.parsed.mnem.match(/\d+/) || ["32"];
        bits = parseInt(bits[0]) / 8;
        pointer += ' + ' + (parseInt(offset) / bits).toString();
    }
    pointer = _memory_name + ' + ' + pointer;
    return Base.read_memory(pointer, register.toString(), _bits(instr), instr.parsed.mnem.endsWith('_s'));
};

var _common_store = function(instr, context) {
    context.memory = true;
    var offset = instr.parsed.opd[instr.parsed.opd.length - 1];
    var register = context.stack.pop().toString();
    var pointer = context.stack.pop().toString();
    var bits = instr.parsed.mnem.match(/\d+/) || ["32"];
    bits = parseInt(bits[0]);
    if (offset != '0') {
        pointer += ' + ' + (parseInt(offset) / (bits / 8)).toString();
    }
    pointer = _memory_name + ' + ' + pointer;
    return Base.write_memory(pointer, register.toString(), bits, false);
};

var _wasm_arch = {
    instructions: {
        eq: _conditional,
        ne: _conditional,
        eqz: _conditional_zero,
        nez: _conditional_zero,
        gt_s: _conditional,
        gt_u: _conditional,
        ge_s: _conditional,
        ge_u: _conditional,
        lt_s: _conditional,
        lt_u: _conditional,
        le_s: _conditional,
        le_u: _conditional,
        gt: _conditional,
        ge: _conditional,
        lt: _conditional,
        le: _conditional,
        const: function(instr, context, instructions) {
            var s = StackVar('const ' + _type(instr), instr);
            context.stack.push(s);
            var num = parseInt(instr.parsed.opd[0]);
            if (num > 1023) {
                num = '0x' + num.toString(16);
            }
            return Base.assign(s.toString(), num.toString());
        },
        load: _common_load,
        load8_s: _common_load,
        load8_u: _common_load,
        load16_s: _common_load,
        load16_u: _common_load,
        store: _common_store,
        store8: _common_store,
        store16: _common_store,
        store32: _common_store,
        add: function(instr, context, instructions) {
            return _math(instr, context, instructions, _type(instr), Base.add);
        },
        and: function(instr, context, instructions) {
            return _math(instr, context, instructions, _type(instr), Base.and);
        },
        div_s: function(instr, context, instructions) {
            return _math(instr, context, instructions, _type(instr), Base.divide);
        },
        div_u: function(instr, context, instructions) {
            return _math(instr, context, instructions, _type(instr, true), Base.divide);
        },
        extend_s: function(instr, context, instructions) {
            var s = _set_local(instr, context, instructions, _type(instr), false);
            var b = context.stack.pop();
            context.stack.push(s);
            if (_is_next_local(instr, instructions) < 0) {
                return Base.extend(s.toString('arg'), b.toString(), _type(instr));
            }
            return Base.extend(s.toString(), b.toString(), _type(instr));
        },
        extend_u: function(instr, context, instructions) {
            var s = _set_local(instr, context, instructions, _type(instr), false);
            var b = context.stack.pop();
            context.stack.push(s);
            if (_is_next_local(instr, instructions) < 0) {
                return Base.extend(s.toString('arg'), b.toString(), _type(instr, true));
            }
            return Base.extend(s.toString(), b.toString(), _type(instr, true));
        },
        mul: function(instr, context, instructions) {
            return _math(instr, context, instructions, _type(instr), Base.multiply);
        },
        or: function(instr, context, instructions) {
            return _math(instr, context, instructions, _type(instr), Base.or);
        },
        reinterpret: function(instr, context, instructions) {
            var s = _set_local(instr, context, instructions, _type(instr), false);
            var b = context.stack.pop();
            context.stack.push(s);
            if (_is_next_local(instr, instructions) < 0) {
                return Base.assign(s.toString('arg'), b.toString());
            }
            return Base.assign(s.toString(), b.toString());
        },
        rem_s: function(instr, context, instructions) {
            return _math(instr, context, instructions, _type(instr), Base.module);
        },
        rem_u: function(instr, context, instructions) {
            return _math(instr, context, instructions, _type(instr, true), Base.module);
        },
        shl: function(instr, context, instructions) {
            return _math(instr, context, instructions, _type(instr), Base.shift_left);
        },
        shr_s: function(instr, context, instructions) {
            return _math(instr, context, instructions, _type(instr), Base.shift_right);
        },
        shr_u: function(instr, context, instructions) {
            return _math(instr, context, instructions, _type(instr, true), Base.shift_right);
        },
        sub: function(instr, context, instructions) {
            return _math(instr, context, instructions, _type(instr), Base.subtract);
        },
        wrap: function(instr, context, instructions) {
            var s = _set_local(instr, context, instructions, _type(instr), false);
            var b = context.stack.pop();
            context.stack.push(s);
            if (_is_next_local(instr, instructions) < 0) {
                return Base.extend(s.toString('arg'), b.toString(), _type(instr, true));
            }
            return Base.extend(s.toString(), b.toString(), _type(instr, true));
        },
        trunc_s: function(instr, context, instructions) {
            var s = _set_local(instr, context, instructions, _type(instr), false);
            var b = context.stack.pop();
            context.stack.push(s);
            if (_is_next_local(instr, instructions) < 0) {
                return Base.extend(s.toString('arg'), b.toString(), _type(instr, true));
            }
            return Base.extend(s.toString(), b.toString(), _type(instr, true));
        },
        trunc_u: function(instr, context, instructions) {
            var s = _set_local(instr, context, instructions, _type(instr), false);
            var b = context.stack.pop();
            context.stack.push(s);
            if (_is_next_local(instr, instructions) < 0) {
                return Base.extend(s.toString('arg'), b.toString(), _type(instr, true));
            }
            return Base.extend(s.toString(), b.toString(), _type(instr, true));
        },
        xor: function(instr, context, instructions) {
            return _math(instr, context, instructions, _type(instr), Base.xor);
        },
        get_global: function(instr, context, instructions) {
            var dst = _set_global(instr, context, instructions, 'int32_t');
            context.stack.push(dst);
            return Base.nop();
        },
        set_global: function(instr, context, instructions) {
            var dst = _set_global(instr, context, instructions, 'int32_t');
            var arg = context.stack.pop();
            return Base.assign(dst.toString(), arg ? arg.toString() : "?");
        },
        get_local: function(instr, context, instructions) {
            context.stack.push(_set_local(instr, context, instructions, 'int32_t', true));
            return Base.nop();
        },
        set_local: function(instr, context, instructions) {
            var dst = _set_local(instr, context, instructions, 'int32_t', false);
            var pos = _is_previous_call(instr, instructions);
            if (pos > -1) {
                var call = instructions[pos];
                call.code = Base.assign(dst.toString(), call.code);
                return Base.nop();
            }
            var arg = context.stack.pop();
            return Base.assign(dst.toString(), arg ? arg.toString() : "?");
        },
        tee_local: function(instr, context, instructions) {
            var dst = _set_local(instr, context, instructions, 'int32_t', true);
            var pos = _is_previous_call(instr, instructions);
            if (pos > -1) {
                var call = instructions[pos];
                call.code = Base.assign(dst.toString(), call.code);
                return Base.nop();
            }
            var arg = context.stack[context.stack.length - 1];
            return Base.assign(dst.toString(), arg.toString());
        },
        drop: function(instr, context, instructions) {
            _remove_const(instr, instructions, context.stack.pop());
            return Base.nop();
        },
        call: function(instr, context, instructions) {
            var args = [];
            for (var i = instructions.indexOf(instr) - 1; i >= 0; i--) {
                var previous = instructions[i].parsed.mnem;
                if (!previous.startsWith('get_') && previous != 'const') {
                    break;
                }
                args.unshift(context.stack.pop());
            }
            if (instr.parsed.opd[0].match(/^\d+$/)) {
                return Base.call('fcn_' + instr.parsed.opd[0], args);
            }
            return Base.call(instr.parsed.opd[0], args);
        },
        return: function(instr, context, instructions) {
            var ret = null;
            if (context.stack.length > 0) {
                if (context.stack.length > 1) {
                    Global().warning('[wasm] stack len is not zero: ' + context.stack.length);
                }
                context.returned = context.stack.pop();
                ret = _remove_const(instr, instructions, context.returned);
                if (!Extra.is.string(ret)) {
                    ret = context.returned.toString();
                }
            }
            return Base.return(ret);
        },
        'if': function(instr, context, instructions) {
            _set_instruction_conditional(instr, context);
            return Base.nop();
        },
        br_if: function(instr, context, instructions) {
            _set_instruction_conditional(instr, context);
            return Base.nop();
        },
        'else': function(instr, context, instructions) {
            return Base.nop();
        },
        block: function(instr, context, instructions) {
            return Base.nop();
        },
        loop: function(instr, context, instructions) {
            return Base.nop();
        },
        end: function(instr, context, instructions) {
            if (instr.jump && instr.jump.lte(instr.location)) {
                _set_instruction_conditional(instr, context);
            } else {
                var curidx = instructions.indexOf(instr);
                if (curidx == (instructions.length - 1)) {
                    var ret = null;
                    if (context.stack.length > 0) {
                        if (context.stack.length > 1) {
                            Global().warning('[wasm] stack len is not zero: ' + context.stack.length);
                        }
                        context.returned = context.stack.pop();
                        ret = _remove_const(instr, instructions, context.returned);
                        if (!Extra.is.string(ret)) {
                            ret = ret.toString();
                        }
                    }
                    return Base.return(ret);
                }
            }

            return Base.nop();
        },
        br: function(instr, context, instructions) {
            return Base.nop();
        },
        nop: function(instr, context, instructions) {
            return Base.nop();
        },
        invalid: function(instr, context, instructions) {
            return Base.nop();
        }
    },
    parse: function(asm) {
        asm = asm.trim().replace(/\s+/g, ' ').replace(/\//g, ' ');
        var type = asm.match(/[if]\d\d\./);
        if (type) {
            type = type[0];
            asm = asm.replace(/[if]\d\d\./, '');
        }
        asm = asm.split(' ');

        return {
            type: type || 'i32',
            mnem: asm.shift(),
            opd: asm
        };
    },
    context: function() {
        return {
            returned: null,
            global: {},
            input: {},
            local: {},
            stack: [],
            condstack: [],
            memory: false,
        };
    },
    globalvars: function(context) {
        var to_return = Extra.to.array(context.global).map(_to_argument);
        if (context.memory) {
            to_return.unshift("extern uint8_t " + _memory_name + "[]");
        }
        return to_return;
    },
    localvars: function(context) {
        var vars = [];
        for (var k in context.local) {
            var o = context.local[k];
            if (Extra.is.inObject(context.input, o) || Extra.is.inObject(context.global, o)) {
                continue;
            }
            vars.push(o.toString('arg'));
        }
        return vars;
    },
    arguments: function(context) {
        return Extra.to.array(context.input).map(_to_argument);
    },
    returns: function(context) {
        if (context.returned) {
            return context.returned.type.replace(/const\s/, '');
        }
        return 'void';
    }
};

export default _wasm_arch;
